package secondary_classes;
import java.io.File;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;

/**
 * @author Rohit
 *
 */
public class OrderBook {
	
	DateFormat dateFormat = new SimpleDateFormat("dd-MMM-yy");
	DateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
	
	/**
	 * Specifies the type of amend to make to an order
	 */
	public static enum AmendType {DEC_VOL, INC_VOL, PRICE};

	private SortedList bids, asks, trades;
	private String name;
	private Date date, time;
	private ArrayList<OpeningOrder> cumulative;
	private BigDecimal referencePrice;

	public int numBids() {
		return bids.size();
	}
	
	public int numAsks() {
		return asks.size();
	}
	
	public int numTrades() {
		return trades.size();
	}
	
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Date getDate() {
		return date;
	}

	public void setDate(Date date) {
		this.date = date;
	}

	public Date getTime() {
		return time;
	}

	public void setTime(Date time) {
		this.time = time;
	}

	public OrderBook(String name) {
		bids = new SortedList(new BidComparatorPriceTime());
		asks = new SortedList(new AskComparatorPriceTime());
		trades = new SortedList(new TradeComparatorTime());
		this.name = name;
		referencePrice = new BigDecimal("-1");
	}

	/**
	 * Inserts a record into the order book
	 * @param o The record to be entered
	 * @throws Exception 
	 */
	public void enter(Order o) throws Exception {
		if (o == null) {
			throw new Exception("Cannot add null order to the order book called " + this.name);
		}
		
		if (o.getBidOrAsk().equals("B")) {
			if (!bids.contains(o)) {
				bids.add(o);
			}
		} else if (o.getBidOrAsk().equals("A")) {
			if (!asks.contains(o)) {
				asks.add(o);
			}
		} else if (o.getBidOrAsk().equals("") && o.getRecordType().equals("TRADE")) {
			if (!trades.contains(o)) {
				trades.add(o);
			}
		} else {
			throw new Exception("Cannot enter order of unknown type");
		}
		this.setDate(o.getDate());
		this.setTime(o.getTime());
	}

	/**
	 * Deletes a Record from the order book.  Use {@link #findOrder(Long)} to retrieve a record using it's Order ID
	 * @param o The order to delete
	 * @throws Exception 
	 */
	public void delete(Order o) throws Exception {
		if (o == null) {
			throw new Exception("Cannot delete null order in the order book called " + this.name);
		}

		if(o.getBidOrAsk().equals("B")) {
			bids.remove(o);
		} else {
			asks.remove(o);
		}
	}

	/**
	 * Amends a record's volume in the order book
	 * @param amendType The type of amend to make. Use {@link AmendType} to specify
	 * @param toBeChanged The order to amend. Use {@link #findOrder(Long)} to retrieve a record using it's Order ID
	 * @param o Order with the amended values
	 * @throws Exception If the amend was not accepted. Use {@link Exception#getMessage()} for details
	 */
	public void amend(AmendType amendType, Order toBeChanged, Order o) throws Exception {
		if (o == null) {
			throw new Exception("Cannot amend an order with a null order in the order book called " + this.name);
		}
		if (toBeChanged == null) {
			throw new Exception("Cannot amend a null order in the order book called " + this.name);
		}
		
		boolean amend = false;

		switch (amendType) {
		case DEC_VOL : {
			if (o.getVolume().compareTo(toBeChanged.getVolume()) >= 0) {
				throw new Exception("Order " + o.getOrderId() + ": Amended volume must be less than the existing volume");
			} else {
				amend = true;
			}
		} break;

		case INC_VOL : {
			if (o.getVolume().compareTo(toBeChanged.getVolume())<= 0) {
				throw new Exception("Order " + o.getOrderId() + ": Amended volume must be greater than the existing volume");
			} else if (o.getDate().before(toBeChanged.getDate()) ||
					(o.getDate().equals(toBeChanged.getDate()) && (o.getTime().before(toBeChanged.getTime())))) {
				throw new Exception("Order " + o.getOrderId() + ": When increasing volume, new date and time must be after the original order's date and time");
			} else {
				amend = true;
			}
		} break;

		case PRICE : {
			if (o.getPrice().equals(toBeChanged.getPrice())) {
				throw new Exception("Order " + o.getOrderId() + ": Amended price must be different to existing price");
			} else if (o.getDate().before(toBeChanged.getDate()) ||
					(o.getDate().equals(toBeChanged.getDate()) && (o.getTime().before(toBeChanged.getTime())))) {
				throw new Exception("Order " + o.getOrderId() + ": When amending price, new date and time must be after the original order's date and time");
			} else {
				amend = true;
			}
		} break;
		}

		if (amend) {
			this.delete(toBeChanged);
			this.enter(o);
		}
	}

	/**
	 * Finds a record when given an order ID
	 * @param orderId the Order ID
	 * @return the record to be found, otherwise null if it doesn't exist in this order book
	 * @throws Exception If the order could not be found
	 */
	public Order findOrder(BigInteger orderId) throws Exception {
		Comparator<Order> comp;
		SortedList list;
		int index;
		
		comp = new OrdersComparatorBidId();
		list = new SortedList(comp);
		list.addAll(bids);
		index = Collections.binarySearch(list, new Order(orderId, "B"), comp);
		if (index >= 0) {
			return list.get(index);
		}
		
		comp = new OrdersComparatorAskId();
		list = new SortedList(comp);
		list.addAll(asks);
		index = Collections.binarySearch(list, new Order(orderId, "A"), comp);
		if (index >= 0) {
			return list.get(index);
		}
		
//		for (Order o : bids) {
//			if (o.getOrderId().equals(orderId)) {
//				return o;
//			}
//		}
//		
//		for (Order o : asks) {
//			if (o.getOrderId().equals(orderId)) {
//				return o;
//			}
//		}
		
		//throw new Exception("Could not find order: " + TradingEngine.i);
		return null;
	}

	public BigDecimal getSpread() {
		if(asks.size() != 0 && bids.size() != 0) {
			return (asks.get(0).getPrice().subtract(bids.get(0).getPrice()));
		} else {
			return new BigDecimal("1");
		}

	}


	public String toString() {
		String result = "";

		result += "    Bids\n";
		for (Order o : bids) {
			result += o.toString() + "\n";
		}

		result += "    Asks\n";
		for (Order r : asks) {
			result += r.toString() + "\n";
		}

		result += "    Trades\n";
		for (Order t : trades) {
			result += t.toString();
			if (!t.equals(trades.get(trades.size() - 1))) {
				result += "\n";
			}
		}

		return result;
	}

	/**
	 * Matching algorithm. Makes the trades
	 * @param date the current date
	 * @param time the current time
	 * @author PJEKH
	 * @throws Exception 
	 */
	public void match(Date date, Date time) throws Exception {
		BigInteger transID = new BigInteger("0");
		while (getSpread().compareTo(new BigDecimal("0")) <= 0) {
			Order bid = bids.get(0);
			Order ask = asks.get(0);
			
			String[] order = {bid.getInstrument(), dateFormat.format(date), timeFormat.format(time), "TRADE", determinePrice(bid, ask).toPlainString(), bid.getVolume().toString(), determinePrice(bid, ask).multiply(new BigDecimal(bid.getVolume())).toPlainString(), transID.toString(), bid.getBidId().toString(), ask.getAskId().toString(), "", "", "", "", "", ""};
			Order trade = new Order(order);
			if (bid.getVolume().equals(ask.getVolume())) {
				this.enter(trade);
				this.delete(bid);
				this.delete(ask);
			} else {
				Order less = ask, more = bid;
				String bidIdString = more.getBidId().toString(), askIdString = "";
				if (ask.getVolume().compareTo(bid.getVolume()) > 0) {
					less = bid;
					more = ask;
					bidIdString = "";
					askIdString = more.getAskId().toString();
				}

				//trades.add(trade);
				this.enter(trade);

				BigInteger volume = more.getVolume().subtract(less.getVolume());
				String[] updatedOrder = {more.getInstrument(), dateFormat.format(more.getDate()), timeFormat.format(more.getTime()), more.getRecordType(), more.getPrice().toPlainString(), volume.toString(), more.getPrice().multiply(new BigDecimal(volume)).toPlainString(), more.getTransId().toString(), bidIdString, askIdString, more.getBidOrAsk(), "", "", "", "", ""};
				Order updated = new Order(updatedOrder);

				if (less.equals(bid)) {
					this.delete(bid);
				} else if (less.equals(ask)) {
					this.delete(ask);
				}
				AmendType a = more.getAmendType(updated);
				amend(a, more, updated);
			}
		}

	}


	/**
	 * Determines which price should be used for the trade.
	 * @param bid the bid order
	 * @param ask the ask order
	 * @return returns the correct price
	 * @author PJEKH
	 */
	private BigDecimal determinePrice(Order bid, Order ask) {
		if(bid.getPrice().equals(ask.getPrice()) || earlier(bid, ask)) {
			return bid.getPrice();
		}
		else {
			return ask.getPrice();
		}
	}

	private boolean earlier(Order bid, Order ask) {
		if(bid.getDate().before(ask.getDate())) {
			return true;
		} else if (bid.getDate().equals(ask.getDate()) && bid.getTime().before(ask.getTime())) {
			return true;
		} else {
			return false;
		}
	}

	public String getMarketDepthFileContents(int marketDepthSize, File outputFile) throws Exception {

		String result = "";

		// Print values

		result += this.getName() + "," + (new SimpleDateFormat("dd-MMM-yy")).format(this.getDate()) +
		"," + (new SimpleDateFormat("HH:mm:ss.SS")).format(this.getTime()) + "," + "Market Depth" + ",";

		int i = 0;

		Iterator<Order> iterBid = bids.iterator();
		Iterator<Order> iterAsk = asks.iterator();
		Order bid; 
		Order ask;
		BigInteger bidVolume = BigInteger.ZERO;
		BigInteger askVolume = BigInteger.ZERO;
		if(iterBid.hasNext()) {
			bid = iterBid.next();
			bidVolume = bid.getVolume();
		} else {
			bid = null;
		}
		if(iterAsk.hasNext()) {
			ask = iterAsk.next();
			askVolume = ask.getVolume();
		} else  {
			ask = null;
		}

		Iterator<Order> iterBid2 = bids.iterator();
		Iterator<Order> iterAsk2 = asks.iterator();
		Order bid2; 
		Order ask2;
		if(iterBid2.hasNext()) {
			bid2 = iterBid2.next();
			if(iterBid2.hasNext()) {
				bid2 = iterBid2.next();
			} else {
				bid2 = null;
			}
		} else {
			bid2 = null;
		}
		if(iterAsk2.hasNext()) {
			ask2 = iterAsk2.next();
			if(iterAsk2.hasNext()) {
				ask2 = iterAsk2.next();
			} else {
				ask2 = null;
			}
		} else {
			ask2 = null;
		}

		int numBuyers = 1;
		int numSellers = 1;

		while (i < marketDepthSize) {
			//bids
			if(bid == null) {
				result += ",,,";
			} else {

				while(bid2 != null && bid.getPrice().equals(bid2.getPrice()) ) { 
					numBuyers++;
					bidVolume = bidVolume.add(bid2.getVolume());
					if(iterBid.hasNext()) {
						bid = iterBid.next();
					} else {
						bid = null;
					}
					if(iterBid2.hasNext()) {
						bid2 = iterBid2.next();
					} else {
						bid2 = null;
					}

				}

				result += bid.getPrice() + "," + bidVolume + "," + numBuyers + ",";
				

				if(iterBid.hasNext()) {
					bid = iterBid.next();
					numBuyers = 1;
					bidVolume = bid.getVolume();
				} else {
					bid = null;
				}
				if(iterBid2.hasNext()) {
					bid2 = iterBid2.next();
				} else {
					bid2 = null;
				}
			}
		
		
		//asks
		
		if(ask == null) {
			result += ",,,";
		} else {

			while(ask2 != null && ask.getPrice().equals(ask2.getPrice())) { 
				numBuyers++;
				askVolume = askVolume.add(ask2.getVolume());
				if(iterAsk.hasNext()) {
					ask = iterAsk.next();
				} else {
					ask = null;
				}
				if(iterAsk2.hasNext()) {
					ask2 = iterAsk2.next();
				} else {
					ask2 = null;
				}

			}

			result += ask.getPrice() + "," + askVolume + "," + numSellers + ",";
			

			if(iterAsk.hasNext()) {
				ask = iterAsk.next();
				numSellers = 1;
				askVolume = ask.getVolume();
			} else {
				ask = null;
			}
			if(iterAsk2.hasNext()) {
				ask2 = iterAsk2.next();
			} else {
				ask2 = null;
			}
		}
	
		i++;
		}

		return result;
	}

	public ArrayList<Order> getTrades() {
		return this.trades;
	}

	/*public static SortedList getAllTrades() {
		return allTrades;
	}*/
	
	/**
	 * The algorithm that runs in the phase: Opening
	 * @param date the current date
	 * @param time the current time
	 * @author PJEKH
	 * @throws Exception 
	 **/
	public void opening(Date date, Date time) throws Exception {
		BigInteger transID = new BigInteger("0");
		BigDecimal price = openPrice();
		while (getSpread().compareTo(BigDecimal.ZERO) <= 0) {
			Order bid = bids.get(0);
			Order ask = asks.get(0);

			if (bid.getVolume().equals(ask.getVolume())) {
				String[] order = {bid.getInstrument(), dateFormat.format(date), timeFormat.format(time), "TRADE", price.toPlainString(), bid.getVolume().toString(), price.multiply(new BigDecimal(bid.getVolume())).toPlainString(), transID.toString(), bid.getBidId().toString(), ask.getAskId().toString(), "", "", "", "", "", ""};
				Order trade = new Order(order);
				this.enter(trade);
				this.delete(bid);
				this.delete(ask);
			} else {
				Order less = ask, more = bid;
				String bidIdString = more.getBidId().toString(), askIdString = "";
				if (ask.getVolume().compareTo(bid.getVolume()) > 0) {
					less = bid;
					more = ask;
					bidIdString = "";
					askIdString = more.getAskId().toString();
				}

				String[] order = {bid.getInstrument(), dateFormat.format(date), timeFormat.format(time), "TRADE", price.toPlainString(), less.getVolume().toString(), price.multiply(new BigDecimal(less.getVolume())).toPlainString(), transID.toString(), bid.getBidId().toString(), ask.getAskId().toString(), "", "", "", "", "", ""};
				Order trade = new Order(order);
				trades.add(trade);

				BigInteger volume = more.getVolume().subtract(less.getVolume());
				String[] updatedOrder = {more.getInstrument(), dateFormat.format(more.getDate()), timeFormat.format(more.getTime()), more.getRecordType(), more.getPrice().toPlainString(), volume.toString(), more.getPrice().multiply(new BigDecimal(volume)).toPlainString(), more.getTransId().toString(), bidIdString, askIdString, more.getBidOrAsk(), "", "", "", "", ""};
				Order updated = new Order(updatedOrder);

				if (less.equals(bid)) {
					this.delete(bid);
				} else if (less.equals(ask)) {
					this.delete(ask);
				}
				AmendType a = more.getAmendType(updated);
				amend(a, more, updated);
			}
		}

	}

	/**
	 * Calculates the price for trades in the phase: Opening
	 * @return price
	 * @author PJEKH
	 * @throws Exception
	 */
	private BigDecimal openPrice() {
		
		BigDecimal price = new BigDecimal("-1");
		
		cumulative = new ArrayList<OpeningOrder>();
		
		try {
			price = principle1();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		

		return price;
			
	}



	private BigDecimal principle1() throws Exception {
		
		if(bids.isEmpty() || asks.isEmpty()) {
			return new BigDecimal("-1");
		}
		
		Iterator<Order> iterBid = bids.iterator();
		Iterator<Order> iterBid2 = bids.iterator();	
		Order cur; 
		Order fut;
		BigInteger sum = BigInteger.ZERO;
		BigInteger total = BigInteger.ZERO;
		BigDecimal price = new BigDecimal("-1");
		
		// Initialize pointers
		
		
		if(iterBid.hasNext()) {
			cur = iterBid.next();
			sum = cur.getVolume();
			price = cur.getPrice();
		} else {
			throw new Exception("No bids - dont make any trades");
		}
		
		if(iterBid2.hasNext()) {
			fut = iterBid2.next();
			if(iterBid2.hasNext()) {
				fut = iterBid2.next();
			} else {
				fut = null;
			}
		} else {
			fut = null;
		}
		
		
		//start
		
		

		while (!asks.isEmpty() && price.compareTo(asks.get(0).getPrice()) >= 0) {
			while(fut != null && cur.getPrice().equals(fut.getPrice())){
				if(iterBid.hasNext()) {
					cur = iterBid.next();
					sum = sum.add(cur.getVolume());
				} else {
					cur = null;
				}
				if(iterBid2.hasNext()) {
					fut = iterBid2.next();
				} else {
					fut = null;
				}
				
			}
			if (cur != null && price.equals(cur.getPrice())) {
				total = sum;
				if(iterBid.hasNext()) {
					cur = iterBid.next();
					sum = sum.add(cur.getVolume());
				} else {
					cur = null;
				}
				if(iterBid2.hasNext()) {
					fut = iterBid2.next();
				} else {
					fut = null;
				}
				
			}
			
			cumulative.add(new OpeningOrder(total, price));
		
			price = price.subtract(new BigDecimal(".001"));
		}
		
		
		
		sum = BigInteger.ZERO;
		price = new BigDecimal("-1");
		total = BigInteger.ZERO;
		
		
		Iterator<Order> iterAsk = asks.iterator();
		Iterator<Order> iterAsk2 = asks.iterator();	
		
		
		// Initialize pointers
		
		
		if(iterAsk.hasNext()) {
			cur = iterAsk.next();
			sum = cur.getVolume();
			price = cur.getPrice();
		} else {
			throw new Exception("No asks - dont make any trades");
		}
		
		if(iterAsk2.hasNext()) {
			fut = iterAsk2.next();
			if(iterAsk2.hasNext()) {
				fut = iterAsk2.next();
			} else {
				fut = null;
			}
		} else {
			fut = null;
		}
		
		// start
		
		int i = cumulative.size()-1;
		
		while (!bids.isEmpty() && price.compareTo(bids.get(0).getPrice()) <= 0) {
			while(fut != null && cur.getPrice().equals(fut.getPrice())){
				if(iterAsk.hasNext()) {
					cur = iterAsk.next();
					sum = sum.add(cur.getVolume());
				} else {
					cur = null;
				}
				if(iterAsk2.hasNext()) {
					fut = iterAsk2.next();
				} else {
					fut = null;
				}
				
			}
			if (cur != null && price.equals(cur.getPrice())) {
				total = sum;
				if(iterAsk.hasNext()) {
					cur = iterAsk.next();
					sum = sum.add(cur.getVolume());
				} else {
					cur = null;
				}
				if(iterAsk2.hasNext()) {
					fut = iterAsk2.next();
				} else {
					fut = null;
				}
				
			}
			
			if(i < 0) {
				System.err.print("indexing wrong");
			}
			cumulative.get(i).setSellVolume(total);
			cumulative.get(i).setMaxExecutableVolume();
			
			i--;
			
			price = price.add(new BigDecimal(".001"));
		}
		
		BigInteger max = new BigInteger("-1");
		
		for (int i1 = 0; i1 < cumulative.size(); i1++) {
			if (cumulative.get(i1).getMaxExecutableVolume().compareTo(max) < 0) {
				cumulative.remove(i1);
				i1--;
			} else if (cumulative.get(i1).getMaxExecutableVolume().compareTo(max) > 0) {
				max = cumulative.get(i1).getMaxExecutableVolume();
			}
		}
		
		for (int i1 = 0; i1 < cumulative.size(); i1++) {
			if (cumulative.get(i1).getMaxExecutableVolume().compareTo(max) < 0) {
				cumulative.remove(i1);
				i1--;
			}
		}
		
		if(cumulative.size() == 1) {
			return cumulative.get(0).getPrice();
		} else {
			return principle2();
		}
		
	}

	/**
	 * Second principle algorithm
	 * @return
	 * @author PJEKH 
	 */
	private BigDecimal principle2() {
		
		// set MinSurplus
		for (int i = 0; i < cumulative.size(); i++) {
			cumulative.get(i).setMinSurplus();
		}
		
		BigInteger min = cumulative.get(0).getMinSurplus().multiply(cumulative.get(0).getMinSurplus());
		OpeningOrder o;
		
		// find lowest MinSurplus
		for (int i = 0; i < cumulative.size(); i++) {
			o = cumulative.get(i);
			if (o.getMinSurplus().multiply(o.getMinSurplus()).compareTo(min) > 0) {
				cumulative.remove(o);
				i--;
			} else if (o.getMinSurplus().multiply(o.getMinSurplus()).compareTo(min) < 0) {
				min = o.getMinSurplus().multiply(o.getMinSurplus());
			}
		}
		
		//remove what was missed in the first pass
		for (int i = 0; i < cumulative.size(); i++) {
			o = cumulative.get(i);
			if (o.getMinSurplus().multiply(o.getMinSurplus()).compareTo(min) > 0) {
				cumulative.remove(o);
				i--;
			}
		}
		
		if(cumulative.size() == 1) {
			return cumulative.get(0).getPrice();
		} else {
			return principle3();
		}
	}

	/**
	 * Third principle algorithm
	 * @return
	 * @author PJEKH
	 */
	private BigDecimal principle3() {
		boolean zero = true;
		OpeningOrder min = null, max = null;
		int i, j;
		
		// looks for a sign change
		for(i = 0, j = 1; j < cumulative.size(); i++, j++) {
			if(cumulative.get(i).getMinSurplus().multiply(cumulative.get(j).getMinSurplus()).compareTo(BigInteger.ZERO) < 0) {
				zero = false;
				break;
			}
		}
		
		// if there's a sign change then we pass the two orders to principle4. Else we either pass
		// the zero flag to principle4 or return
		if (!zero) {
			if (cumulative.get(i).getPrice().compareTo(cumulative.get(j).getPrice()) < 0) {
				min = cumulative.get(i);
				max = cumulative.get(j);
			} else {
				min = cumulative.get(j);
				max = cumulative.get(i);
			}
			return principle4(zero, min, max);
		} else if (cumulative.get(j-1).getMinSurplus().equals(BigInteger.ZERO)) {
			return principle4(zero, null, null);
		} else {
			if (cumulative.get(j-1).getMinSurplus().compareTo(BigInteger.ZERO) > 0) {
				return cumulative.get(0).getPrice();
			} else {
				return cumulative.get(cumulative.size()-1).getPrice();
			}
		}
	}
	
	/**
	 * Fourth principle algorithm
	 * @param zero flag to show if all the miniimumSurplus values are 0
	 * @param a minimum order, set to null if zero flag is true
	 * @param b maximum order, set to null if zero flag is true
	 * @return
	 */
	private BigDecimal principle4(boolean zero, OpeningOrder a, OpeningOrder b) {
	
		OpeningOrder min, max;
		
		if(zero) {
			min = cumulative.get(cumulative.size()-1);
			max = cumulative.get(0);
		} else {
			min = a;
			max = b;
		}
		
		if(referencePrice.compareTo(BigDecimal.ZERO) < 0) {
			return min.getPrice();
		}else if (referencePrice.compareTo(max.getPrice()) >= 0) {
			return max.getPrice();
		} else if (referencePrice.compareTo(min.getPrice()) <= 0) {
			return min.getPrice();
		} else {
			return referencePrice;
		}
		
	}

	public void closing() {
		referencePrice = openPrice();
		
	}

}
